Skip to content

feat(mcp): OAuth 2.1 + PKCE for outbound MCP servers#4441

Open
waleedlatif1 wants to merge 12 commits into
stagingfrom
waleedlatif1/mcp-oauth
Open

feat(mcp): OAuth 2.1 + PKCE for outbound MCP servers#4441
waleedlatif1 wants to merge 12 commits into
stagingfrom
waleedlatif1/mcp-oauth

Conversation

@waleedlatif1
Copy link
Copy Markdown
Collaborator

Summary

  • Adds spec-compliant OAuth 2.1 + PKCE support for outbound MCP servers via the SDK's OAuthClientProvider
  • Auto-detects OAuth requirement on server create via unauthenticated probe (WWW-Authenticate / oauth-protected-resource)
  • Persists per-user-per-server tokens (encrypted) in new mcp_server_oauth table; SDK refreshes automatically before expiry
  • Popup-based consent flow (/api/mcp/oauth/start/api/mcp/oauth/callback) with state CSRF protection
  • Pre-registered OAuth client support (Client ID + Secret in Advanced settings) for servers without RFC 7591 DCR
  • Surfaces reauth_required from tool execution when refresh token is invalid so the UI can prompt to reconnect

Type of Change

  • New feature

Testing

Tested manually against OAuth-protected MCP servers (Linear). Existing header-auth servers regression-checked.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel
Copy link
Copy Markdown

vercel Bot commented May 5, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped May 12, 2026 6:10am

Request Review

@cursor
Copy link
Copy Markdown

cursor Bot commented May 5, 2026

PR Summary

High Risk
High: introduces new OAuth token storage, callback/start endpoints, and server CRUD behavior that affects authentication, secret encryption, and connection state; mistakes could break tool execution or leak/mishandle credentials.

Overview
Adds first-class OAuth 2.1 + PKCE support for outbound MCP servers, including new /api/mcp/oauth/start and /api/mcp/oauth/callback routes that drive a popup-based authorization flow and refresh tools after successful token exchange.

Extends MCP server CRUD to support authType plus optional pre-registered oauthClientId/oauthClientSecret (encrypted at rest), returns hasOauthClientSecret instead of the secret, auto-detects OAuth vs header auth on create, and revokes/clears stored OAuth state (best-effort RFC7009) when URL/credentials change or on delete.

Updates MCP client/service/connection-manager to use an SDK authProvider for OAuth servers, serialize token refreshes, share workspace-scoped OAuth connections, and surface re-auth required as 401 from discover/execute so the UI can prompt reconnect; UI adds advanced OAuth fields and “Connect with OAuth” actions, plus query-key/mutation refactors and new tests/migration for mcp_server_oauth.

Reviewed by Cursor Bugbot for commit 8310051. Configure here.

Comment thread apps/sim/lib/mcp/oauth/provider.ts
Comment thread apps/sim/hooks/queries/mcp.ts
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 5, 2026

Greptile Summary

This PR adds spec-compliant OAuth 2.1 + PKCE support for outbound MCP servers, including auto-detection via an unauthenticated probe, a popup-based consent flow, encrypted per-server token persistence in a new mcp_server_oauth table, and pre-registered client credential support. The previous review threads have been thoroughly addressed — workspace boundary checks before revocation, stale-token cleanup on credential changes, per-server connecting state, and the UnauthorizedError / reauth_required surface are all correctly implemented.

  • Adds SimMcpOauthProvider implementing the SDK's OAuthClientProvider with DB-backed PKCE, encrypted token/client storage, and a process-local refresh lock to prevent concurrent token races within a single process.
  • New /api/mcp/oauth/start and /api/mcp/oauth/callback routes drive the popup flow; state is hashed in the DB and burned before token exchange; workspace/user ownership is verified in the callback before any credential writes.
  • The mcp_server_oauth table uses a unique index on mcpServerId (one shared OAuth connection per server, intentionally workspace-wide) with onDelete: cascade on the MCP server foreign key.

Confidence Score: 5/5

The OAuth flow is safe to merge; all previously identified workspace boundary and token-management issues have been addressed.

The implementation correctly handles SSRF validation before the auth-type probe, burns state before token exchange, verifies workspace ownership in both the callback and revocation paths, and encrypts all OAuth secrets at rest. The remaining nits are minor: a silent no-op on the discovery invalidation scope, a benign type mismatch in the refresh lock, and a low-probability TOCTOU UX issue where two concurrent users could overwrite each other's PKCE state. None of these affect correctness under normal usage.

No files require special attention; all critical paths have been validated.

Important Files Changed

Filename Overview
apps/sim/lib/mcp/oauth/provider.ts New SimMcpOauthProvider implementing the SDK OAuthClientProvider; correctly handles PKCE, token storage/refresh, and DCR short-circuiting. The discovery scope in invalidateCredentials is silently ignored.
apps/sim/lib/mcp/oauth/storage.ts Encrypted DB persistence for OAuth artifacts; hashed state lookups, safeDecrypt with column clear on key-rotation errors, and a process-local refresh lock. Minor prev.then(fn, fn) signature mismatch in withMcpOauthRefreshLock.
apps/sim/app/api/mcp/oauth/callback/route.ts New OAuth callback handler: validates state, burns it before token exchange, verifies user/workspace ownership, exchanges code for tokens, then triggers tool discovery.
apps/sim/app/api/mcp/oauth/start/route.ts Initiates the OAuth 2.1 + PKCE flow; validates server, blocks conflicting active flows, returns the authorization URL. A TOCTOU gap allows concurrent callers to overwrite each other's PKCE state.
apps/sim/app/api/mcp/servers/route.ts POST/GET/DELETE routes enhanced with OAuth support: auto-detects auth type via probe after SSRF validation, persists encrypted OAuth credentials, clears stale tokens on credential/URL change.
apps/sim/app/api/mcp/servers/[id]/route.ts PATCH route early-returns 404 before revocation logic when server does not belong to workspace; clears OAuth tokens transactionally when URL or credentials change.
apps/sim/lib/mcp/service.ts Service updated to route OAuth MCP connections through SimMcpOauthProvider with the refresh lock; both discovery paths treat auth errors as disconnected rather than error.
apps/sim/lib/mcp/oauth/revoke.ts Best-effort RFC 7009 token revocation; correct auth selection, scoped timeout, never throws.
apps/sim/lib/mcp/oauth/probe.ts Sends a standard MCP initialize request to detect OAuth requirement; https-only plus loopback, 5s timeout, returns headers as safe default on any failure.
apps/sim/hooks/queries/mcp.ts Adds useStartMcpOauth mutation with client-side https scheme validation before window.open; refactors query keys to hierarchical form.
apps/sim/hooks/mcp/use-mcp-oauth-popup.ts Manages OAuth popup lifecycle: per-server connecting state, interval-based popup-closed detection, and postMessage listener scoped to same origin.
packages/db/schema.ts Adds auth_type, oauth_client_id, oauth_client_secret to mcpServers; new mcp_server_oauth table with correct cascade deletes and indexes.
apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal.tsx Adds Advanced settings panel with OAuth credential fields; oauthClientSecretTouched prevents accidental secret erasure on edit.
apps/sim/app/workspace/[workspaceId]/settings/components/mcp/mcp.tsx Integrates useMcpOauthPopup for per-server OAuth button state; adds Connect with OAuth button for disconnected OAuth servers.
apps/sim/app/api/mcp/tools/execute/route.ts Adds OAuth-specific 401 handling (reauth_required) for auth errors; fixes timeout handle leak; improves type safety for schema property iteration.

Sequence Diagram

sequenceDiagram
    participant UI as Browser (MCP Settings)
    participant Start as /api/mcp/oauth/start
    participant AS as Authorization Server
    participant CB as /api/mcp/oauth/callback
    participant DB as mcp_server_oauth (DB)
    participant SDK as MCP SDK (mcpAuth)

    UI->>Start: "GET ?serverId=&workspaceId="
    Start->>DB: getOrCreateOauthRow(serverId, userId, workspaceId)
    DB-->>Start: row
    Start->>SDK: mcpAuth(provider, serverUrl)
    SDK-->>Start: throw McpOauthRedirectRequired(authorizationUrl)
    Start-->>UI: "{status: redirect, authorizationUrl}"
    UI->>AS: window.open(authorizationUrl) [popup]
    Note over UI,AS: User approves in popup
    AS->>CB: "GET /callback?code=&state="
    CB->>DB: loadOauthRowByState(hashState(state))
    DB-->>CB: row (TTL-gated)
    CB->>DB: clearState(row.id)
    CB->>SDK: mcpAuth(provider, authorizationCode)
    SDK->>AS: POST /token (PKCE code_verifier)
    AS-->>SDK: "{access_token, refresh_token}"
    SDK->>DB: saveTokens(row.id, tokens) [encrypted]
    CB->>DB: clearVerifier(row.id)
    CB-->>UI: HTML postMessage mcp-oauth, window.close
    UI->>UI: invalidate server/tool queries
Loading

Reviews (30): Last reviewed commit: "fix(mcp): strip oauthClientSecret from o..." | Re-trigger Greptile

Comment thread apps/sim/app/workspace/[workspaceId]/settings/components/mcp/mcp.tsx Outdated
Comment thread apps/sim/lib/mcp/oauth/storage.ts
Comment thread apps/sim/lib/mcp/oauth/storage.ts
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

Greptile summary findings addressed in f587e82:

  • Edit modal drops existing OAuth Client ID: editInitialData now includes oauthClientId; the API already returns it (only the secret is masked) so the field populates and Advanced auto-expands.
  • Shared OAuth mutation disables all buttons: per-server pending tracked in a local Set<string>; the spinner is scoped to the card whose flow is in progress.
  • Plaintext PKCE codeVerifier: now encrypted at rest via encryptSecret to match tokens/clientInformation.

The point about clearing a pre-registered Client ID by emptying the field is a follow-up — oauthClientId || undefined collapses an intentional clear into a no-op. Will tackle when adding TTL cleanup for abandoned OAuth sessions.

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/api/mcp/servers/[id]/route.ts
Comment thread apps/sim/app/api/mcp/servers/[id]/route.ts
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/api/mcp/servers/[id]/route.ts Outdated
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/api/mcp/servers/route.ts Outdated
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/api/mcp/tools/execute/route.ts
Comment thread apps/sim/lib/mcp/service.ts Outdated
Comment thread apps/sim/lib/mcp/service.ts Outdated
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

Comment thread apps/sim/app/api/mcp/oauth/callback/route.ts
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/mcp/oauth/storage.ts
Comment thread apps/sim/app/api/mcp/oauth/start/route.ts
Comment thread apps/sim/app/api/mcp/oauth/callback/route.ts Outdated
Comment thread apps/sim/lib/mcp/oauth/revoke.ts
…e check

- withMcpOauthRefreshLock now attaches cleanup via .then(cleanup, cleanup)
  so cleanup is awaited as part of the lock chain and rejections don't
  produce unhandled promise rejection warnings.
- revoke.ts: !res.ok already implies status !== 200; drop the redundant
  conjunction.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/mcp/oauth/storage.ts
Comment thread apps/sim/app/api/mcp/oauth/callback/route.ts
Comment thread apps/sim/app/api/mcp/servers/[id]/route.ts
Look up the OAuth row by state once at the top of the callback so the
serverId can be threaded into provider_error, missing_params, and
unauthenticated closes. Without it, the popup's postMessage omits
serverId and the parent UI can't clear the connecting state until the
500ms popup-closed poll fires.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/mcp/oauth/revoke.ts
revokeMcpOauthTokens bailed when row.clientInformation was null, which is
always the case for pre-registered clients (SimMcpOauthProvider.saveClientInformation
is a no-op when preregistered is set). Fall back to mcpServers.oauthClientId so
revocation proceeds for the pre-registered path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/api/mcp/oauth/callback/route.ts
Comment thread apps/sim/app/api/mcp/oauth/callback/route.ts Outdated
- Burn the state row on the provider-error path so a replayed callback with the
  same state cannot complete a token exchange after the user saw the error.
- Read the OAuth state row once at the top of the callback and reuse it on the
  happy path, eliminating a redundant query and a TTL-boundary race where a
  legitimate flow could be marked invalid_state between two reads.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

Comment thread apps/sim/hooks/queries/mcp.ts
The useUpdateMcpServer optimistic update was spreading the raw updates object
into the TanStack Query cache, which briefly placed the plaintext
oauthClientSecret in client-side cache. Strip it from the optimistic spread —
the cache holds the server-sanitized record after onSettled invalidation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 8310051. Configure here.

id: data.id,
workspaceId: variables.workspaceId,
name: variables.config.name,
transport: variables.config.transport,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create mutation leaks plaintext secret in TanStack state

Medium Severity

The useCreateMcpServer mutation's mutationFn returns ...serverData which includes the plaintext oauthClientSecret from the user's input. TanStack Query persists this as mutation.data in its internal state. The PR author explicitly fixed the equivalent issue for useUpdateMcpServer by stripping oauthClientSecret from the optimistic update via const { oauthClientSecret: _omitSecret, ...safeUpdates } = updates, but the create mutation was missed. The plaintext secret remains accessible through React DevTools or TanStack Query state until the mutation is garbage collected.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8310051. Configure here.

: null,
state: row.state,
updatedAt: row.updatedAt,
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated OAuth row decryption/mapping logic across functions

Low Severity

loadOauthRow and loadOauthRowByState contain nearly identical ~20-line blocks that decrypt and map a raw DB row into an McpOauthRow. This duplicated decryption logic (for clientInformation, tokens, and codeVerifier) means a future fix or change to the mapping must be applied in both places, risking inconsistency.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8310051. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant